package com.sharp.schedule.config;

import com.sharp.framework.aop.publish.RedisPublish;
import com.sharp.kernel.constant.CommonConst;
import com.sharp.framework.service.redis.RedisService;
import com.sharp.kernel.utils.BeanUtil;
import com.sharp.kernel.utils.DateUtil;
import com.sharp.kernel.utils.ReflectionUtil;
import com.sharp.schedule.dao.entity.SaSchedule;
import com.sharp.schedule.dao.entity.SaScheduleHistory;
import com.sharp.schedule.dao.repository.SaScheduleHistoryService;
import com.sharp.schedule.dao.repository.SaScheduleService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronSequenceGenerator;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * Title: TaskSchedulerConfig
 * Description: Copyright: Copyright (c) 2019 Company: BHFAE
 *
 * @author Sharp
 * @date 2020/10/15 21:56
 */
@Slf4j
@Component
public class TaskSchedulerConfig {

    public static final String SINGLE = "0";
    public static final String CRON = "1";
    public static final String RATE = "2";
    public static final String DELAY = "3";
    private static final String REDIS_LOCK_KEY = "Redisson:";
    @Resource
    private ApplicationContext applicationContext;
    private Map<String, ScheduledFuture<?>> map = new ConcurrentHashMap<>();

    @Resource
    private ThreadPoolTaskScheduler threadPoolTaskScheduler;
    @Resource
    private SaScheduleService saScheduleService;
    @Resource
    private SaScheduleHistoryService saScheduleHistoryService;
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private RedisService redisService;

    @RedisPublish
    public void updateTask(SaSchedule schedule) {
        log.info("更新Task:{}", schedule);
        delTask(schedule, true);
        if (StringUtils.equals(schedule.getStatus(), CommonConst.YES)) {
            addTask(schedule);
        }
    }

    public void addLocalTask(SaSchedule schedule) {
        addTask(schedule);
    }

    public void updateLocalTask(SaSchedule schedule) {
        updateTask(schedule);
    }

    public Boolean delLocalTask(SaSchedule schedule) {
        return delTask(schedule);
    }

    @RedisPublish
    public Boolean delTask(SaSchedule schedule) {
        forceReleaseLock(schedule);
        return delTask(schedule, true);
    }

    private void forceReleaseLock(SaSchedule schedule) {
        redisService.delete(getSyncKey(schedule));
    }

    private Boolean delTask(SaSchedule schedule, boolean mayInterruptIfRunning) {
        ScheduledFuture<?> toBeRemovedFuture = map.remove(getSyncKey(schedule));
        if (toBeRemovedFuture != null) {
            return toBeRemovedFuture.cancel(mayInterruptIfRunning);
        } else {
            return false;
        }
    }

    @RedisPublish
    public void addTask(SaSchedule schedule) {
        if (validateSchedule(schedule)) {
            add(getSyncKey(schedule), buildFuture(schedule));
        }
    }

    @RedisPublish
    public Boolean stopTask(SaSchedule schedule) {
        forceReleaseLock(schedule);
        return delTask(schedule, false);
    }

    private TaskSchedulerConfig getSelf() {
        return applicationContext.getBean(TaskSchedulerConfig.class);
    }

    @PostConstruct
    public void init() {
        try {
            List<SaSchedule> scheduleList = saScheduleService.getAllScheduleForInit();
            if (CollectionUtils.isNotEmpty(scheduleList)) {
                for (SaSchedule schedule : scheduleList) {
                    if (validateSchedule(schedule)) {
                        add(getSyncKey(schedule), buildFuture(schedule));
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.info("不支持配置管理定时任务");
        }
    }

    private void add(String key, ScheduledFuture<?> future) {
        if (future != null) {
            map.put(key, future);
        }
    }

    private ScheduledFuture<?> buildFuture(SaSchedule schedule) {
        switch (schedule.getType()) {
            case SINGLE:
                return threadPoolTaskScheduler.schedule(getRunnable(schedule),
                        schedule.getStartTime());

            case CRON:
                return threadPoolTaskScheduler.schedule(getRunnable(schedule), getCronTrigger(schedule));

            case RATE:
                return threadPoolTaskScheduler.scheduleAtFixedRate(getRunnable(schedule),
                        schedule.getStartTime(), schedule.getPeriod());

            case DELAY:
                return threadPoolTaskScheduler.scheduleWithFixedDelay(getRunnable(schedule),
                        schedule.getStartTime(), schedule.getPeriod());
            default:
                return null;
        }
    }

    private Runnable getRunnable(SaSchedule schedule) {
        return () -> {
            if (!validateSchedule(schedule)) {
                delTask(schedule);
            }
            log.info("执行定时任务: {}", schedule);
            SaScheduleHistory history = ReflectionUtil.copy(schedule, SaScheduleHistory.class);
            Integer retry = schedule.getRetry();
            Object service = applicationContext.getBean(StringUtils.uncapitalize(schedule.getService()));
            Method method = getMethod(schedule, service);
            RLock lock = tryLock(schedule, service, method);
            if (lock == null) {
                log.info("没有获取锁");
                return;
            }

            history.setRunAt(new Date());
            do {
                try {
                    if (StringUtils.isNotBlank(schedule.getParameter())) {
                        ReflectionUtil.invoke(service, method, schedule.getParameter());
                    } else {
                        ReflectionUtil.invoke(service, method);
                    }
                    history.setFinishAt(new Date());
                    history.setResult(CommonConst.YES);
                    break;
                } catch (Exception e) {
                    log.error("定时任务执行出错", e);
                    history.setFinishAt(new Date());
                    history.setResult(CommonConst.NO);
                }
            } while (retry != null && retry-- != 0);

            releaseLock(lock, schedule);
            if (schedule.getId() != null) {
                updateHistory(history, schedule);
                updateSchedule(history, schedule);
            }
        };
    }

    private void releaseLock(RLock lock, SaSchedule schedule) {
        if (StringUtils.equals(schedule.getType(), SINGLE)) {
            if (lock != null) {
                lock.forceUnlock();
            }
        }
    }

    private Trigger getCronTrigger(SaSchedule schedule) {
        return triggerContext -> {
            CronTrigger trigger = new CronTrigger(schedule.getCron());
            return trigger.nextExecutionTime(triggerContext);
        };
    }

    private RLock tryLock(SaSchedule schedule, Object service, Method method) {
        String syncKey = getSyncKey(schedule);
        RLock lock = redissonClient.getLock(syncKey);
        boolean res = false;
        switch (schedule.getType()) {
            case SINGLE:
                res = lock.tryLock();
                break;
            case CRON:
                Date now = new Date();
                CronSequenceGenerator generator = new CronSequenceGenerator(schedule.getCron());
                Date next = generator.next(now);

                try {
                    long lockTime = DateUtil.secondDiff(now, next) - 1;
                    log.info("锁定时间: {}", lockTime);
                    if (lockTime > 0) {
                        res = lock.tryLock(0, lockTime, TimeUnit.SECONDS);
                    } else {
                        res = true;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
            case RATE:
            case DELAY:
                try {
                    long lockTime = schedule.getPeriod() - 1;
                    if (lockTime > 0) {
                        res = lock.tryLock(0, lockTime, TimeUnit.SECONDS);
                    } else {
                        res = true;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                break;
            default:
                break;
        }
        return res ? lock : null;
    }

    private void updateHistory(SaScheduleHistory history, SaSchedule schedule) {
        history.setId(null);
        history.setScheduleId(schedule.getId());
        saScheduleHistoryService.insertSelective(history);
    }

    private void updateSchedule(SaScheduleHistory history, SaSchedule schedule) {
        if (StringUtils.equals(schedule.getTemporary(), CommonConst.YES)) {
            saScheduleService.disableByPrimaryKey(schedule);
            getSelf().delTask(schedule);
        } else {
            boolean update = true;
            switch (schedule.getType()) {
                case SINGLE: {
                    schedule.setStatus(CommonConst.NO);
                    break;
                }
                case RATE: {
                    Date next = DateUtil.addSeconds(schedule.getStartTime(), schedule.getPeriod());
                    schedule.setStartTime(next);
                    break;
                }
                case DELAY: {
                    Date next = DateUtil.addSeconds(history.getFinishAt(), schedule.getPeriod());
                    schedule.setStartTime(next);
                    break;
                }
                case CRON:
                default:
                    update = false;
                    break;
            }
            if (update) {
                saScheduleService.updateByPrimaryKeySelective(schedule);
                getSelf().updateTask(schedule);
            }
        }

    }

    public ScheduledFuture<?> get(SaSchedule schedule) {
        return map.get(getSyncKey(schedule));
    }

    private String getSyncKey(SaSchedule schedule) {
        Object service = applicationContext.getBean(StringUtils.uncapitalize(schedule.getService()));
        Method method = getMethod(schedule, service);
        StringBuilder syncKeyBuilder = new StringBuilder(REDIS_LOCK_KEY);
        String serviceName = service.getClass().getSimpleName();
        int endIndex = serviceName.indexOf("$$");
        if (endIndex != -1) {
            serviceName = serviceName.substring(0, endIndex);
        }
        syncKeyBuilder.append(serviceName).append(".").append(method.getName()).append(".").append(schedule.hashCode());
        return syncKeyBuilder.toString();
    }

    private Method getMethod(SaSchedule schedule, Object service) {
        Method method;
        if (StringUtils.isNotBlank(schedule.getParameter())) {
            method = ReflectionUtil.getMethod(service.getClass(), schedule.getFunction(), String.class);
        } else {
            method = ReflectionUtil.getMethod(service.getClass(), schedule.getFunction());
        }
        return method;
    }

    private boolean validateSchedule(SaSchedule schedule) {
        Object serviceBean = applicationContext.getBean(StringUtils.uncapitalize(schedule.getService()));
        String methodName = schedule.getFunction();
        try {
            if (StringUtils.isNotBlank(schedule.getParameter())) {
                BeanUtil.validateBeanFunction(serviceBean, methodName, String.class);
            } else {
                BeanUtil.validateBeanFunction(serviceBean, methodName);
            }
            return StringUtils.equals(schedule.getStatus(), CommonConst.YES);
        } catch (Exception e) {
            log.info("定时任务服务验证未通过");
            return false;
        }
    }
}
